// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/download/base_file.h"

#include <cguid.h>
#include <objbase.h>
#include <shellapi.h>
#include <windows.h>

#include "base/files/file.h"
#include "base/files/file_util.h"
#include "base/guid.h"
#include "base/macros.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_restrictions.h"
#include "content/browser/download/download_interrupt_reasons_impl.h"
#include "content/browser/download/download_stats.h"
#include "content/public/browser/browser_thread.h"
#include "net/log/net_log_event_type.h"

namespace content {
namespace {

    const int kAllSpecialShFileOperationCodes[] = {
        // Should be kept in sync with the case statement below.
        ERROR_ACCESS_DENIED,
        ERROR_SHARING_VIOLATION,
        ERROR_INVALID_PARAMETER,
        0x71,
        0x72,
        0x73,
        0x74,
        0x75,
        0x76,
        0x78,
        0x79,
        0x7A,
        0x7C,
        0x7D,
        0x7E,
        0x80,
        0x81,
        0x82,
        0x83,
        0x84,
        0x85,
        0x86,
        0x87,
        0x88,
        0xB7,
        0x402,
        0x10000,
        0x10074,
    };

    // Maps the result of a call to |SHFileOperation()| onto a
    // |DownloadInterruptReason|.
    //
    // These return codes are *old* (as in, DOS era), and specific to
    // |SHFileOperation()|.
    // They do not appear in any windows header.
    //
    // See http://msdn.microsoft.com/en-us/library/bb762164(VS.85).aspx.
    DownloadInterruptReason MapShFileOperationCodes(int code)
    {
        DownloadInterruptReason result = DOWNLOAD_INTERRUPT_REASON_NONE;

        // Check these pre-Win32 error codes first, then check for matches
        // in Winerror.h.
        // This switch statement should be kept in sync with the list of codes
        // above.
        switch (code) {
        // Not a pre-Win32 error code; here so that this particular case shows up in
        // our histograms. Unfortunately, it is used not just to signal actual
        // ACCESS_DENIED errors, but many other errors as well. So we treat it as a
        // transient error.
        case ERROR_ACCESS_DENIED: // Access is denied.
            result = DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR;
            break;

        // This isn't documented but returned from SHFileOperation. Sharing
        // violations indicate that another process had the file open while we were
        // trying to rename. Anti-virus is believed to be the cause of this error in
        // the wild. Treated as a transient error on the assumption that the file
        // will be made available for renaming at a later time.
        case ERROR_SHARING_VIOLATION:
            result = DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR;
            break;

        // This is also not a documented return value of SHFileOperation, but has
        // been observed in the wild. We are treating it as a transient error based
        // on the cases we have seen so far.  See http://crbug.com/368455.
        case ERROR_INVALID_PARAMETER:
            result = DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR;
            break;

        // The source and destination files are the same file.
        // DE_SAMEFILE == 0x71
        case 0x71:
            result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
            break;

        // The operation was canceled by the user, or silently canceled if the
        // appropriate flags were supplied to SHFileOperation.
        // DE_OPCANCELLED == 0x75
        case 0x75:
            result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
            break;

        // Security settings denied access to the source.
        // DE_ACCESSDENIEDSRC == 0x78
        case 0x78:
            result = DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED;
            break;

        // The source or destination path exceeded or would exceed MAX_PATH.
        // DE_PATHTOODEEP == 0x79
        case 0x79:
            result = DOWNLOAD_INTERRUPT_REASON_FILE_NAME_TOO_LONG;
            break;

        // The path in the source or destination or both was invalid.
        // DE_INVALIDFILES == 0x7C
        case 0x7C:
            result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
            break;

        // The destination path is an existing file.
        // DE_FLDDESTISFILE == 0x7E
        case 0x7E:
            result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
            break;

        // The destination path is an existing folder.
        // DE_FILEDESTISFLD == 0x80
        case 0x80:
            result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
            break;

        // The name of the file exceeds MAX_PATH.
        // DE_FILENAMETOOLONG == 0x81
        case 0x81:
            result = DOWNLOAD_INTERRUPT_REASON_FILE_NAME_TOO_LONG;
            break;

        // The destination is a read-only CD-ROM, possibly unformatted.
        // DE_DEST_IS_CDROM == 0x82
        case 0x82:
            result = DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED;
            break;

        // The destination is a read-only DVD, possibly unformatted.
        // DE_DEST_IS_DVD == 0x83
        case 0x83:
            result = DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED;
            break;

        // The destination is a writable CD-ROM, possibly unformatted.
        // DE_DEST_IS_CDRECORD == 0x84
        case 0x84:
            result = DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED;
            break;

        // The file involved in the operation is too large for the destination
        // media or file system.
        // DE_FILE_TOO_LARGE == 0x85
        case 0x85:
            result = DOWNLOAD_INTERRUPT_REASON_FILE_TOO_LARGE;
            break;

        // The source is a read-only CD-ROM, possibly unformatted.
        // DE_SRC_IS_CDROM == 0x86
        case 0x86:
            result = DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED;
            break;

        // The source is a read-only DVD, possibly unformatted.
        // DE_SRC_IS_DVD == 0x87
        case 0x87:
            result = DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED;
            break;

        // The source is a writable CD-ROM, possibly unformatted.
        // DE_SRC_IS_CDRECORD == 0x88
        case 0x88:
            result = DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED;
            break;

        // MAX_PATH was exceeded during the operation.
        // DE_ERROR_MAX == 0xB7
        case 0xB7:
            result = DOWNLOAD_INTERRUPT_REASON_FILE_NAME_TOO_LONG;
            break;

        // An unspecified error occurred on the destination.
        // XE_ERRORONDEST == 0x10000
        case 0x10000:
            result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
            break;

        // Multiple file paths were specified in the source buffer, but only one
        // destination file path.
        // DE_MANYSRC1DEST == 0x72
        case 0x72:
            result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
            break;

        // Rename operation was specified but the destination path is
        // a different directory. Use the move operation instead.
        // DE_DIFFDIR == 0x73
        case 0x73:
            result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
            break;

        // The source is a root directory, which cannot be moved or renamed.
        // DE_ROOTDIR == 0x74
        case 0x74:
            result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
            break;

        // The destination is a subtree of the source.
        // DE_DESTSUBTREE == 0x76
        case 0x76:
            result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
            break;

        // The operation involved multiple destination paths,
        // which can fail in the case of a move operation.
        // DE_MANYDEST == 0x7A
        case 0x7A:
            result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
            break;

        // The source and destination have the same parent folder.
        // DE_DESTSAMETREE == 0x7D
        case 0x7D:
            result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
            break;

        // An unknown error occurred.  This is typically due to an invalid path in
        // the source or destination.  This error does not occur on Windows Vista
        // and later.
        // DE_UNKNOWN_ERROR == 0x402
        case 0x402:
            result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
            break;

        // Destination is a root directory and cannot be renamed.
        // DE_ROOTDIR | ERRORONDEST == 0x10074
        case 0x10074:
            result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
            break;
        }

        // Narrow down on the reason we're getting some catch-all interrupt reasons.
        if (result == DOWNLOAD_INTERRUPT_REASON_FILE_FAILED) {
            UMA_HISTOGRAM_CUSTOM_ENUMERATION(
                "Download.MapWinShErrorFileFailed", code,
                base::CustomHistogram::ArrayToCustomRanges(
                    kAllSpecialShFileOperationCodes,
                    arraysize(kAllSpecialShFileOperationCodes)));
        }

        if (result == DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED) {
            UMA_HISTOGRAM_CUSTOM_ENUMERATION(
                "Download.MapWinShErrorAccessDenied", code,
                base::CustomHistogram::ArrayToCustomRanges(
                    kAllSpecialShFileOperationCodes,
                    arraysize(kAllSpecialShFileOperationCodes)));
        }

        if (result == DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR) {
            UMA_HISTOGRAM_CUSTOM_ENUMERATION(
                "Download.MapWinShErrorTransientError", code,
                base::CustomHistogram::ArrayToCustomRanges(
                    kAllSpecialShFileOperationCodes,
                    arraysize(kAllSpecialShFileOperationCodes)));
        }

        if (result != DOWNLOAD_INTERRUPT_REASON_NONE)
            return result;

        // If not one of the above codes, it should be a standard Windows error code.
        return ConvertFileErrorToInterruptReason(
            base::File::OSErrorToFileError(code));
    }

} // namespace

// Renames a file using the SHFileOperation API to ensure that the target file
// gets the correct default security descriptor in the new path.
// Returns a network error, or net::OK for success.
DownloadInterruptReason BaseFile::MoveFileAndAdjustPermissions(
    const base::FilePath& new_path)
{
    base::ThreadRestrictions::AssertIOAllowed();

    // The parameters to SHFileOperation must be terminated with 2 NULL chars.
    base::FilePath::StringType source = full_path_.value();
    base::FilePath::StringType target = new_path.value();

    source.append(1, L'\0');
    target.append(1, L'\0');

    SHFILEOPSTRUCT move_info = { 0 };
    move_info.wFunc = FO_MOVE;
    move_info.pFrom = source.c_str();
    move_info.pTo = target.c_str();
    move_info.fFlags = FOF_SILENT | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_NOCONFIRMMKDIR | FOF_NOCOPYSECURITYATTRIBS;

    int result = SHFileOperation(&move_info);
    DownloadInterruptReason interrupt_reason = DOWNLOAD_INTERRUPT_REASON_NONE;

    if (result == 0 && move_info.fAnyOperationsAborted)
        interrupt_reason = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
    else if (result != 0)
        interrupt_reason = MapShFileOperationCodes(result);

    if (interrupt_reason != DOWNLOAD_INTERRUPT_REASON_NONE)
        return LogInterruptReason("SHFileOperation", result, interrupt_reason);
    return interrupt_reason;
}

} // namespace content
